{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#飞翔的小鸟app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上一章，通过制作2048app，我们已经掌握了游戏开发的简单技巧。这一章，我们继续游戏开发，做一个同样很受欢迎的游戏，飞翔的小鸟（Flappy Bird），重点学习一下游戏开发中的横向卷轴模式（Side-Scrolling）。\n",
    "\n",
    "<!-- TEASER_END-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这是一款由越南独立开发者[阮哈东（Dong Nguyen）](http://tech.qq.com/a/20140210/001859.htm)2013年开发的手机游戏，短时间竟占领了全球各大App Store免费排行榜首位，2014年年末的时候，下载量已经 iOS App Store第一。其设计思路非常有趣，游戏操作就是一个人点击屏幕（或者空格键）保持飞翔，穿过重重障碍。这种简单重复的设计思路现在越来越流行，后面会详细介绍。\n",
    "\n",
    "**移动游戏设计的荆棘之路**\n",
    "\n",
    "经典的二维街机游戏风格在手机上复活了。有大量的经典游戏商业改造版，和30年前唯一不同的就是价签——包括Dizzy，Sonic，Double Dragon和R-Type等等。\n",
    "\n",
    "这些游戏一个共同的不足就是控制方式感受很差，毕竟触摸屏和陀螺仪目前还不能完全替代摇杆的效果。这也给新游戏提供了卖点——发挥触摸屏的特点，设计一种新的控制方式就能获得成功。\n",
    "\n",
    "一些开发者通过简单的设计来赢得客户，因为简单游戏有巨大的市场，尤其是低成本和免费的游戏。\n",
    "\n",
    "那些操作简单的游戏确实很受欢迎，飞翔的小鸟就是如此。这一章，我们将用Kivy来实现这种简单的设计方法。教学大纲如下：\n",
    "\n",
    "- 模拟简单的街机游戏\n",
    "- 用Kivy部件开发游戏，完成方向控制和二维变换，比如旋转\n",
    "- 实现简单的碰撞检测\n",
    "- 实现游戏的声音效果\n",
    "\n",
    "这个游戏没有获胜条件，最小的碰撞都会失败。在原版游戏中，玩家以分数高低论输赢。和上一章类似，如果感兴趣，记分板可以当作练习。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##项目介绍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们要做一个与飞翔的小鸟差不多的版本，姑且取名叫Kivy bird吧。游戏最终界面如下：\n",
    "\n",
    "![kivybird](kbpic/7.1 kivybird.png)\n",
    "\n",
    "我们的游戏包括下面三个部分：\n",
    "\n",
    "- **背景图案**：背景是由一些以不同速度移动哦图层构成，给人一种视差效果。运动速度是不变的，也没有其他游戏事件。背景比较容易做，我们将从这里开始。\n",
    "- **障碍物（管道）**：这是一个单独的图层，也是以固定的速度向玩家移动。与背景不同的是，管道的高度会不断变化，中间留出一段空间让玩家通过。碰到管道游戏失败。\n",
    "- **游戏角色（小鸟）**：小鸟一直往下掉，只能垂直飞翔。玩家点击屏幕，小鸟就向上飞。如果小鸟掉到地上，碰到天花板或管道，游戏都失败。\n",
    "\n",
    "这就是游戏的基本设计思路。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##制作背景动画"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们将用下面的图片来做背景图案：\n",
    "\n",
    "![background](kbpic/7.2 background.png)\n",
    "\n",
    "这些图片都可以无缝平铺在一起——这并不是必须的，只是看着会更好看。\n",
    "\n",
    "如上所述，背景一直是运动的。这种效果可以通过两种方法实现：\n",
    "\n",
    "- 直接的方法就是在背景上移动一个大的多边形（或者几个多边形）。只是创建循环的动画需要费点功夫\n",
    "- 更有效的方法是创建一些静态多边形（一个是一层）占据整个屏幕，然后让花纹图案动起来。用一个平铺的花纹图案，这个方法可以流畅的实现动画效果，也省不少功夫——不需要重新定位背景上的对象。\n",
    "\n",
    "我们要第二种方法来实现，因为这更简单有效。首先让我们把`kivybird.kv`文件做出来：\n",
    "\n",
    "```yaml\n",
    "FloatLayout:\n",
    "    Background:\n",
    "        id: background\n",
    "        canvas:\n",
    "            Rectangle:\n",
    "                pos: self.pos\n",
    "                size: (self.width, 96)\n",
    "                texture: self.tx_floor\n",
    "            Rectangle:\n",
    "                pos: (self.x, self.y + 96)\n",
    "                size: (self.width, 64)\n",
    "                texture: self.tx_grass\n",
    "            Rectangle:\n",
    "                pos: (self.x, self.height - 144)\n",
    "                size: (self.width, 128)\n",
    "                texture: self.tx_cloud\n",
    "```\n",
    "\n",
    ">这里的数字都是花纹的尺寸：96是地面高度，64是草的高度，144是云的高度。在实际开发中写这些代码很费劲，不过我们应该尽量简化代码，降低工作量。\n",
    "\n",
    "你会看到，这里没有移动的部分，就是三个矩形在屏幕的底部和顶部。动画效果需要花纹用`Background`类中带`tx_`的属性来实现，下面我们就是。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###加载平铺的花纹"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们建一个辅助函数来加载平铺的花纹，这个函数在后面经常用到，所以把它放在最上面。\n",
    "\n",
    "首先创建一个`Widget`类，作为自定义部件的基类，`main.py`中代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.core.image import Image\n",
    "from kivy.uix.widget import Widget\n",
    "\n",
    "class BaseWidget(Widget):\n",
    "    def load_tileable(self, name):\n",
    "        t = Image('%s.png' % name).texture\n",
    "        t.wrap = 'repeat'\n",
    "        setattr(self, 'tx_%s' % name, t)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "创建辅助函数的语句就是`t.wrap = 'repeat'`。我们要把它应用到每一块花纹上。\n",
    "\n",
    "我们还需要储存新加载的花纹，用`tx_`加图片名称来命名。比如，`load_tileable('grass')`就会把`grass.png`加载到`self.tx_grass`属性。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###背景部件"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们来实现`Background`部件："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.properties import ObjectProperty\n",
    "\n",
    "class Background(BaseWidget):\n",
    "    tx_floor = ObjectProperty(None)\n",
    "    tx_grass = ObjectProperty(None)\n",
    "    tx_cloud = ObjectProperty(None)\n",
    "    \n",
    "    def __init__(self, **kwargs):\n",
    "        super(Background, self).__init__(**kwargs)\n",
    "        for name in ('floor', 'grass', 'cloud'):\n",
    "            self.load_tileable(name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果现在执行代码，你会看到花纹被拉伸填充矩形，这是因为还没有指定花纹的坐标。改变每块花纹的`uvsize`属性就可以了，这样就计算出覆盖多边形需要多少块花纹了。比如，`uvsize`设为`(2, 2)`表示填充一个矩形需要4块花纹。\n",
    "\n",
    "辅助函数可以用来设置`uvsize`的值，这样我们的花纹就不会变形了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def set_background_size(self, tx):\n",
    "    tx.uvsize = (self.width / tx.width, -1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">这里负坐标值表示花纹可以被切割。Kivy用这种效果来避免高成本的栅格操作，把负担转给GPU（显卡），这样处理起来更轻松。\n",
    "\n",
    "这个方法依赖于背景的宽度，所以每次`size`属性变化之后可以用`on_size() `调用一次。这样就可以在屏幕发生变化的时候保持`uvsize`属性及时更新了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def on_size(self, *args):\n",
    "    for tx in (self.tx_floor, self.tx_grass, self.tx_cloud):\n",
    "        self.set_background_size(tx)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在背景图案就变成这样了：\n",
    "![texturebackground](kbpic/7.3 texturebackground.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###背景动画"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面我们要让背景动起来。首先，我们要在`KivyBirdApp`类增加一个每秒60下的运动计时器："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.app import App\n",
    "from kivy.clock import Clock\n",
    "\n",
    "class KivyBirdApp(App):\n",
    "    def on_start(self):\n",
    "        self.background = self.root.ids.background\n",
    "        Clock.schedule_interval(self.update, 0.016)\n",
    "        \n",
    "    def update(self, nap):\n",
    "        self.background.update(nap)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`update()`方法就是把控制传递给`Background`部件的`update()`。当我们需要更多移动的时候，我们再扩展这个方法。\n",
    "\n",
    "在`Background.update()`里面，我们改变花纹来模拟运动状态："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update(self, nap):\n",
    "    self.set_background_uv('tx_floor', 2 * nap)\n",
    "    self.set_background_uv('tx_grass', 0.5 * nap)\n",
    "    self.set_background_uv('tx_cloud', 0.1 * nap)\n",
    "    \n",
    "def set_background_uv(self, name, val):\n",
    "    t = getattr(self, name)\n",
    "    t.uvpos = ((t.uvpos[0] + val) % self.width, t.uvpos[1])\n",
    "    self.property(name).dispatch(self)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "辅助函数里面的`set_background_uv()`作用是：\n",
    "\n",
    "- 增加`uvpos`属性的横坐标，水平移动花纹\n",
    "- 花纹的属性调用`dispatch()`表示花纹位置已经改变了\n",
    "\n",
    "`kivybird.kv`的画布指令会监听这个变化并及时反馈，把花纹重新渲染出来，这样就会看到流畅的动画了。\n",
    "\n",
    "`set_background_uv()`里面控制不同图层速度的因子是随意选择的，可以自定义。\n",
    "\n",
    "这样背景就完成了，下面我们来做管道。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##制作管道"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "管道分成两部分：高的和低的。中间会留出一个孔给小鸟飞过。每一部分都是有不同长度的管体和管头构成。\n",
    "\n",
    "![pipe](kbpic/7.4 pipe.png)\n",
    "\n",
    "`kivybird.kv`文件里的布局部件给我们一个好起点：\n",
    "\n",
    "```yaml\n",
    "<Pipe>:\n",
    "    canvas:\n",
    "        Rectangle:\n",
    "            pos: (self.x + 4, self.FLOOR)\n",
    "            size: (56, self.lower_len)\n",
    "            texture: self.tx_pipe\n",
    "            tex_coords: self.lower_coords\n",
    "        Rectangle:\n",
    "            pos: (self.x, self.FLOOR + self.lower_len)\n",
    "            size: (64, self.PCAP_HEIGHT)\n",
    "            texture: self.tx_pcap\n",
    "        Rectangle:\n",
    "            pos: (self.x + 4, self.upper_y)\n",
    "            size: (56, self.upper_len)\n",
    "            texture: self.tx_pipe\n",
    "            tex_coords: self.upper_coords\n",
    "        Rectangle:\n",
    "            pos: (self.x, self.upper_y - self.PCAP_HEIGHT)\n",
    "            size: (64, self.PCAP_HEIGHT)\n",
    "            texture: self.tx_pcap\n",
    "    size_hint: (None, 1)\n",
    "    width: 64\n",
    "```\n",
    "\n",
    "其实很简单，就是把管道从下到上分成四个矩形：\n",
    "\n",
    "- 底部管体\n",
    "- 底部管头\n",
    "- 顶部管体\n",
    "- 顶部管头\n",
    "\n",
    "![kvpipe](kbpic/7.5 kvpipe.png)\n",
    "\n",
    "与`Background`部件的实现过程类似，这些属性都要连接到部件图形显示算法的Python代码中。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####管道属性介绍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`pipe`部件有趣的属性是："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.properties import (AliasProperty, \n",
    "                             ListProperty,\n",
    "                             NumericProperty,\n",
    "                             ObjectProperty)\n",
    "class Pipe(BaseWidget):\n",
    "    FLOOR = 96\n",
    "    PCAP_HEIGHT = 26\n",
    "    PIPE_GAP = 120\n",
    "    tx_pipe = ObjectProperty(None)\n",
    "    tx_pcap = ObjectProperty(None)\n",
    "    ratio = NumericProperty(0.5)\n",
    "    lower_len = NumericProperty(0)\n",
    "    lower_coords = ListProperty((0, 0, 1, 0, 1, 1, 0, 1))\n",
    "    upper_len = NumericProperty(0)\n",
    "    upper_coords = ListProperty((0, 0, 1, 0, 1, 1, 0, 1))\n",
    "    upper_y = AliasProperty(\n",
    "        lambda self: self.height - self.upper_len,\n",
    "        None, bind=['height', 'upper_len'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，常量都放在`ALL_CAPS`里面：\n",
    "\n",
    "- `FLOOR`：地面花纹的高度\n",
    "- `PCAP_HEIGHT`：管头高度\n",
    "- `PIPE_GAP`：留给小鸟飞过的小孔高度\n",
    "\n",
    "然后就是花纹的属性`tx_pipe`和`tx_pcap`。它们和那些在`Background`类里面花纹的用法一样"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Pipe(BaseWidget):\n",
    "    def __init__(self, **kwargs):\n",
    "        super(Pipe, self).__init__(**kwargs)\n",
    "        \n",
    "        for name in ('pipe', 'pcap'):\n",
    "            self.load_tileable(name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`ratio`属性定义空的位置：`0.5`表示出现在中间（默认值），`0`表示出现在屏幕底部（地上），`1`表示出现在屏幕顶部（天空）。\n",
    "\n",
    "`upper_y`是减少输入次数的辅助函数，它是用来计算`height - upper_len`值的。\n",
    "\n",
    "还有两个重要的属性`lower_coords`和`upper_coords`，用来设置花纹的坐标。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###设置花纹的坐标值"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在`Background`部件的实现过程中，我们调整了花纹的属性，像`uvsize`和`uvpos`来控制渲染效果。这个方法的问题是这么做会影响花纹的所有实例。\n",
    "\n",
    "只要花纹没有在不同的几何形状中使用这么做就没问题。但是，现在我们需要在所有形状中都控制花纹的属性，因此我们就不能调整`uvsize`和`uvpos`了。我们需要用`Rectangle.tex_coords`。\n",
    "\n",
    "`Rectangle.tex_coords`属性接受一个8个数字的列表或元组，把花纹的坐标匹配到矩形的四个角落。`tex_coords`这种匹配方式如下图所示：\n",
    "\n",
    "![coordinatesmap](kbpic/7.6 coordinatesmap.png)\n",
    "\n",
    ">花纹匹配通常用`u`和`v`，不用`x`和`y`。这样可以把几何位置与花纹坐标值区分开，经常容易混淆。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###实现管道"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个主题看着有点混乱，让我们一点点来推进：我们要垂直固定管道上的砖块，只需要调整`tex_coords`的第5和第7个元素。另外，`tex_coords`的值和`uvsize`里面的值是一个意思。基于管道高度调整砖块的坐标如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def set_coords(self, coords, len):\n",
    "    len /= 16 # height of the texture\n",
    "    coords[5:] = (len, 0, len) # set the last 3 items"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后就是用`ratio`和屏幕高度来计算管道的长度："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def on_size(self, *args):\n",
    "    pipes_length = self.height - (\n",
    "        Pipe.FLOOR + Pipe.PIPE_GAP + 2 * Pipe.PCAP_HEIGHT)\n",
    "    self.lower_len = self.ratio * pipes_length\n",
    "    self.upper_len = pipes_length - self.lower_len\n",
    "    self.set_coords(self.lower_coords, self.lower_len)\n",
    "    self.set_coords(self.upper_coords, self.upper_len)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这段`on_size()`代码用来使所有的属性与屏幕尺寸保持同步。要反映`ratio`的变化，需要这样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "self.bind(ratio=self.on_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你可能发现在代码中我们没改变这个属性。这是因为管道的整个生命周期将通过`KivyBirdApp`类来处理，马上你就会看到。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###生成管道"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要创建一堆望不到头的管道森林，我们需要把它们摆满屏幕，用循环队列就可以实现。\n",
    "\n",
    "我们让两个管道的间距是半屏宽，这样可以给玩家充分的准备时间，这样屏幕上同时会出现最多3个管道。为了方便测量，我们需要做4个管道。\n",
    "\n",
    "实现代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class KivyBirdApp(App):\n",
    "    pipes = []\n",
    "    \n",
    "    def on_start(self):\n",
    "        self.spacing = 0.5 * self.root.width\n",
    "        # ...\n",
    "        \n",
    "    def spawn_pipes(self):\n",
    "        for p in self.pipes:\n",
    "            self.root.remove_widget(p)\n",
    "            \n",
    "        self.pipes = []\n",
    "        \n",
    "        for i in range(4):\n",
    "            p = Pipe(x=self.root.width + (self.spacing * i))\n",
    "            p.ratio = random.uniform(0.25, 0.75)\n",
    "            self.root.add_widget(p)\n",
    "            self.pipes.append(p)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`pipes`列表的使用应该考虑实现细节。我们可以遍历子部件列表来连接管道，但是只是更好看一点儿。\n",
    "\n",
    "`spawn_pipes()`方法开始部分的清除代码允许我们后面重启程序更方便。\n",
    "\n",
    "我们还用随机分布来控制`ratio`参数。这里用`[0.25, 0.75]`作为随机范围，而不是常用的`[0, 1]`，是为了让小孔生成的位置更容易一些。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###循环移动管道"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "与背景图案通过改变`uvpos`属性模拟运动的方式不同，管道真正移动。更新`KivyBirdApp.update()`方法来实现管道的循环更新："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update(self, nap):\n",
    "    self.background.update(nap)\n",
    "    \n",
    "    for p in self.pipes:\n",
    "        p.x -= 96 * nap\n",
    "        if p.x <= -64: # pipe gone off screen\n",
    "            p.x += 4 * self.spacing\n",
    "            p.ratio = random.uniform(0.25, 0.75)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "和之前的动画一样，`96`是随机的移动速度因子；因子越大速度越快。\n",
    "\n",
    "每个管道的`ratio`数值都是随机生成的，这样就为玩家创建一个新的管子。界面如下图所示：\n",
    "\n",
    "![movingpipes](kbpic/7.7 movingpipes.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##制作小鸟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "下面我们来制作小鸟：\n",
    "![bird](kbpic/7.8 bird.png)\n",
    "\n",
    "这个很简单，直接用Kivy的`Image`部件（`kivy.uix.image.Image`）实现`Bird`类就行。\n",
    "\n",
    "`kivybird.kv`文件里面，我们需要几个属性来处理小鸟图片：\n",
    "\n",
    "```yaml\n",
    "Bird:\n",
    "    id: bird\n",
    "    pos_hint: {'center_x': 0.3333, 'center_y': 0.6}\n",
    "    size: (54, 54)\n",
    "    size_hint: (None, None)\n",
    "    source: 'bird.png'\n",
    "```\n",
    "\n",
    "这是`Bird`类的Python实现："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.uix.image import Image as ImageWidget\n",
    "\n",
    "class Bird(ImageWidget):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "在实现细节之前，我们需要完成一些基础工作。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###游戏玩法回顾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在，让我们回忆一下游戏的过程：\n",
    "\n",
    "1. 首先，在没有任何管道和重力的时候，确定鸟的初始位置。这个状态用`playing = False`代码表示\n",
    "2. 只要玩家开始了游戏（点击屏幕或者用键盘敲空格键），代码就变成`playing = True`，管道开始生成，重力开始影响小鸟的状态。玩家需要持续的动作保持小鸟不掉下来\n",
    "3. 如果发生碰撞，游戏重回`playing = False`，每个物体都会静止下来，等待玩家重新启动，然后回到步骤2重新开始\n",
    "\n",
    "为了实现这些，我们需要获取玩家输入的内容，很容易做到，因为我们只关心事件是否发生，不关心在哪里发生，整个屏幕就是一个大的按钮。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####接受用户输入"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面是实现代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.core.window import Window, Keyboard\n",
    "class KivyBirdApp(App):\n",
    "    playing = False\n",
    "    def on_start(self):\n",
    "        # ...\n",
    "        Window.bind(on_key_down=self.on_key_down)\n",
    "        self.background.on_touch_down = self.user_action\n",
    "        \n",
    "    def on_key_down(self, window, key, *args):\n",
    "        if key == Keyboard.keycodes['spacebar']:\n",
    "            self.user_action()\n",
    "            \n",
    "    def user_action(self, *args):\n",
    "        if not self.playing:\n",
    "            self.spawn_pipes()\n",
    "            self.playing = True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就是用户输入处理方式：`on_key_down`事件处理键盘输入，检查玩家是否敲了空格键。`on_touch_down`事件处理其他事件。最后都调用`user_action()`方法，执行`spawn_pipes()`，并把`playing`设置成`True`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###实现小鸟上下飞行"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "紧接着，我们要实现重力让小鸟在一个方向上飞行。这里，我们引入`Bird.speed`属性和一个新常量——加速度。每一帧的速度矢量都向下增加，造成一种匀加速下降运行。如下面的代码所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Bird(ImageWidget):\n",
    "    ACCEL_FALL = 0.25\n",
    "    \n",
    "    speed = NumericProperty(0)\n",
    "    \n",
    "    def gravity_on(self, height):\n",
    "        # Replace pos_hint with a value\n",
    "        self.pos_hint.pop('center_y', None)\n",
    "        self.center_y = 0.6 * height\n",
    "        \n",
    "    def update(self, nap):\n",
    "        self.speed -= Bird.ACCEL_FALL\n",
    "        self.y += self.speed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当`playing`变成`True`时，`gravity_on()`方法会被调用。把`self.bird.gravity_on(self.root.height)`插入到` KivyBirdApp.user_action()`方法中："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "if not self.playing:\n",
    "    self.bird.gravity_on(self.root.height)\n",
    "    self.spawn_pipes()\n",
    "    self.playing = True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个方法可以有效的重置鸟的初始位置，从`pos_hint`里面把`'center_y'`移除。\n",
    "\n",
    ">`self.bird`类似前面的`self.background`。下面的代码应该放在`KivyBirdApp.on_start()`里面：\n",
    "\n",
    ">```python\n",
    ">self.background = self.root.ids.background\n",
    ">self.bird = self.root.ids.bird\n",
    ">```\n",
    "\n",
    "我们还得从`KivyBirdApp.update()`方法里面调用`Bird.update()`。这样做有个好处，可以在不玩游戏的时候为升级游戏对象加一个防护："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update(self, nap):\n",
    "    self.background.update(nap)\n",
    "    if not self.playing:\n",
    "        return # don't move bird or pipes\n",
    "    \n",
    "    self.bird.update(nap)\n",
    "    # rest of the code omitted"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你会发现，任何时候`Background.update()`方法都可以被调用；其他方法都是必要的时候才调用。\n",
    "\n",
    "这里没有实现保持小鸟在空中的能力，下面会实现。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####保持在空中"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要让飞翔的小鸟跳着飞行也很简单。我们改写`Bird.speed`就行，把它设置一个正数值，当小鸟持续跌落的时候让它正常延迟。让我们在`Bird`类里面增加方法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "ACCEL_JUMP = 5\n",
    "\n",
    "def bump(self):\n",
    "    self.speed = Bird.ACCEL_JUMP"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在，我们需要在`KivyBirdApp.user_action()`方法的最后调用`self.bird.bump()`就可以了，只要重复点击屏幕或按空格键都可以保持在空中。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###旋转小鸟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "旋转小鸟是为了让游戏更生动，当它飞行的时候，沿着它的飞行轨迹旋转，看着很生动。向上飞行的时候朝着右上角旋转，向下飞行的时候朝着左下角旋转。\n",
    "\n",
    "角度计算的方法如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Bird(ImageWidget):\n",
    "    speed = NumericProperty(0)\n",
    "    angle = AliasProperty(\n",
    "        lambda self: 5 * self.speed,\n",
    "        None, bind=['speed'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里的速度因子`5`是随意设置的。\n",
    "\n",
    "现在，要让小鸟旋转起来，我们要在`kivybird.kv`里面加入：\n",
    "\n",
    "```yaml\n",
    "<Bird>:\n",
    "    canvas.before:\n",
    "        PushMatrix\n",
    "        Rotate:\n",
    "            angle: root.angle\n",
    "            axis: (0, 0, 1)\n",
    "            origin: root.center\n",
    "    canvas.after:\n",
    "        PopMatrix\n",
    "```\n",
    "\n",
    "这个操作会改变OpenGL使用的局部坐标系统，影响后面所有的渲染。不要忘了保存（`PushMatrix`）和恢复（`PopMatrix`）坐标系统的状态，否则致命的错误可能会发生，导致整个画面变形。\n",
    "\n",
    ">如果您遇到莫名的app渲染问题，看看OpenGL的底层指令。\n",
    "\n",
    "这样，小鸟就可以沿着既定的轨道飞行了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##碰撞监测"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个游戏最重要的事情之一就是碰撞监测，当鸟碰到地板、天花板和管道都要结束游戏。\n",
    "\n",
    "用地面和屏幕高度与小鸟的高度`bird.y`对比，就可以轻松确认小鸟是否已经碰到。在`KivyBirdApp`实现如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def test_game_over(self):\n",
    "    if self.bird.y < 90 or \\\n",
    "            self.bird.y > self.root.height - 50:\n",
    "        return True\n",
    "    return False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "监测是否碰到管道有点困难。我们要分两步来监测：首先，我们用Kivy的`collide_widget()`方法来测试横坐标，然后检查纵坐标是否在合理的范围之内（管道上下两段的`lower_len`和`upper_len`属性）。`KivyBirdApp.test_game_over()`方法最终实现如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def test_game_over(self):\n",
    "    screen_height = self.root.height\n",
    "    \n",
    "    if self.bird.y < 90 or \\\n",
    "            self.bird.y > screen_height - 50:\n",
    "        return True\n",
    "    \n",
    "    for p in self.pipes:\n",
    "        if not p.collide_widget(self.bird):\n",
    "            continue\n",
    "            \n",
    "        # The gap between pipes\n",
    "        if (self.bird.y < p.lower_len + 116 or\n",
    "            self.bird.y > screen_height - (\n",
    "                p.upper_len + 75)):\n",
    "            return True\n",
    "        \n",
    "    return False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果监测失败就会返回`False`，游戏会结束。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###游戏结束"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当碰撞发生是回发生什么呢？我们只要把`self.playing`改为`False`就行。监测结果可以在所有的计算完成后增加到`KivyBirdApp.update()`最后："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update(self, nap):\n",
    "    # ...\n",
    "    if self.test_game_over():\n",
    "        self.playing = False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个状态要等用户重新开始游戏才会消失。写碰撞监测代码最给力的部分就是边玩边测试，就是可以同时出现不同的游戏失败状态：\n",
    "\n",
    "![gameover](kbpic/7.9 gameover.png)\n",
    "\n",
    "虽然游戏失败，效果还是很Q的。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##制作声效"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这部分和Kivy开发没啥关系了，就是演示一些制作游戏和应用声效的工具。\n",
    "\n",
    "声效的最大问题都不是技术上的。创建高质量的声效不是简单的事儿，软件工程师毕竟没有音乐和声乐工程师专业。另外，很多应用实际上没用声效，所以声效通常是被忽略了。\n",
    "\n",
    "不过制作声效的简便工具还是不少的。[Bfxr](www.bfxr.net)就是一个很棒的免费电子合成器。用法很简单，就是单击一些设置按钮配置好音效，然后点击**Save to Disk**保存到电脑上就行了，用Bfxr可以很轻松地为app创建声效。\n",
    "\n",
    "![bfxr](kbpic/7.10 bfxr.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###Kivy声音播放"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在程序处理时，Kivy提供了声音播放的API："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.core.audio import SoundLoader\n",
    "snd = SoundLoader.load('sound.wav')\n",
    "snd.play()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "用`play()`方法就开始播放。不过这个简单的方法在游戏里面使用有点问题。\n",
    "\n",
    "很多时候，你需要让声音随着你的动作不断的重复。Kivy的`sound`类的问题就是只能在指定的时间内播放一次。\n",
    "\n",
    "可行方式如下：\n",
    "\n",
    "- 等前一个播放终止（默认的行为，后面的事件都会静音）\n",
    "- 为每个事件停止播放然后重启播放，还是有问题（可能引起延迟）\n",
    "\n",
    "要解决这个问题，我们需要创建一个`sound`对象的队列，这样每次调用`play()`就产生一个`Sound`对象。当队列结束时，我们可以从头开始。只要队列足够长，我们就可以完全不用担心`Sound`的限制了。实际上，长度为10就可以。实现代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class MultiAudio:\n",
    "    _next = 0\n",
    "    \n",
    "    def __init__(self, filename, count):\n",
    "        self.buf = [SoundLoader.load(filename)\n",
    "                    for i in range(count)]\n",
    "        \n",
    "    def play(self):\n",
    "        self.buf[self._next].play()\n",
    "        self._next = (self._next + 1) % len(self.buf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "用法如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "snd = MultiAudio('sound.wav', 5)\n",
    "snd.play()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面第二个参数就是队列的长度。看看我们是如何改写`Sound` API的`play()`方法的。这样在简单的程序里直接替换`Sound`就可以。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###添加声效"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面让我们把声效添加到kivy Bird游戏里。\n",
    "\n",
    "有两个地方需要使用声音文件，一个是小鸟向上飞，一个是撞到东西。\n",
    "\n",
    "第一个事件，通过单击和切换来初始化，在速度快的时候重复很频繁，我们用一个队列。第二个事件，游戏结束，不会频繁的发生，所以就用一个`Sound`对象："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "snd_bump = MultiAudio('bump.wav', 4)\n",
    "snd_game_over = SoundLoader.load('game_over.wav')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "用前面加载过的`MultiAudio`类就行。剩下的事情就是把`play()`添加到适当的位置："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "if self.test_game_over():\n",
    "    snd_game_over.play()\n",
    "    self.playing = False\n",
    "    \n",
    "def user_action(self, *args):\n",
    "    snd_bump.play()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样，飞翔的小鸟就有声音了，希望你喜欢。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##总结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这一章，我们做了一个Kivy小游戏，用到了画布指令和部件。\n",
    "\n",
    "作为UI工具包，Kivy提供了很多好东西，允许我们自由的组合、新建任何部件，可以做微信客户端和视频游戏等等。Kivy属性实现的一个特别值得称赞的地方就是可以无限制的组织数据，帮助我们充分消除不必要的内容（比如在属性没有发生变化的时候重新刷屏）。\n",
    "\n",
    "Kivy的另一个令人惊奇也是反直觉的特点就是它的性能很好——虽然Python并不以性能著称。部分原因是因为Kivy底层系统是Cython写的，被编译成机器码，性能和C语言有一拼。另外，如果配置合适，显卡加速也可以用来保证动画流程运行。\n",
    "\n",
    "下一章我们将继续提供图形渲染性能。\n",
    "\n",
    "[源代码](kbpic/src/7_KivyBird)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
